
繼昨天的泛型函式,今天要來介紹「泛型約束」
keyof 關鍵字我們知道泛型中的 T ,代表一個型別參數(Type Parameter),它可代進任意輸入的型別
我們可以使用 extends 關鍵字來定義「泛型約束」。告訴 TypeScript ,泛型的「型別參數」必須符合特定的型別結構
interface Lengthwise {
  length: number;
}
 
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
  console.log(arg.length);
  return arg;
}
loggingIdentity(3);                        // ❌ Error
loggingIdentity({ length: 10, value: 3 }); // ✅ Pass
loggingIdentity 函式中的泛型約束 <Type extends Lengthwise> 約束了任何傳入的 arg 都必須有一個 length 屬性
loggingIdentity(3) 因為不符合參數必須有 length 這個屬性的規定,所以報錯,錯誤訊息如下Argument of type 'number' is not assignable to parameter of type 'Lengthwise'
附上圖解
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key];
}
let x = { 
  a: 1, 
  b: 2, 
  c: 3, 
  d: 4 
};
 
getProperty(x, "a"); // ✅ Pass
getProperty(x, "m"); // ❌ Error
這個函式中有兩個型別參數,分別是 Type KeyType 可以接受任何類型的值Key 受到 Type 的鍵( = 物件的屬性名)的約束。這也代表 Key 必須是 Type 中存在的鍵,也就是 a, b, c, d,這個約束防止函式被傳入任何不存在的鍵
這也是為何 getProperty(x, 'm') 會報錯,因為 x 裡沒有 m 這個屬性
keyof 是 TypeScript 的操作符,用來取得「物件型別」的所有「鍵」的聯合(union)
這是 TypeScript 中處理物件屬性時很好用的操作符,確保在編譯時,對物件屬性的引用就已是安全的
interface Person {
    name: string;
    age: number;
}
type PersonKeys = keyof Person; // 'name' | 'age'
滑鼠移到上面這段會出現下方的推斷
宣告一個變數指向 PersonKeys,值只能是 name 或 age
雖然陣列和元組等也屬於「物件型別」的一種,但在 keyof 的情境中,「物件型別」通常是指有具名屬性的物件結構,常見於介面(Interface)、類別(Class)和型別別名(Type Alias)
但這不代表 TypeScript 禁止 keyof 用在陣列和元組等其他物件型別,只是這樣做沒什麼意義(等等看下面範例就會知道了😆),TypeScript 對它們的處理方式與一般物件型別略有不同
let numbers: number[] = [1, 2, 3];
type ArrayKeys = keyof typeof numbers;  // type ArrayKeys = number | "length" | "push" | "pop" | ...
當你在陣列中使用 keyof 時,推斷出的是陣列操作方法名稱的 union
let tuple: [string, number] = ["hello", 42];
type TupleKeys = keyof typeof tuple;  // type TupleKeys = "0" | "1" | "length" | "push" | "pop" | ...
元組的 keyof 會回傳 index 和陣列操作方法的名稱
這樣做其實沒什麼意義,對吧
只是單純抱有實驗精神把實驗結果記錄一下😂
泛型介面(Generic Interface)的定義方式類似於泛型函式。你可以指定一個或多個型別參數,這些參數可以用於介面中的屬性、方法
interface Container<T> {
    value: T;
    add(value: T): void;
}
泛型類別(Generic Classes)會在 Class Name 後會加上型別參數,表示該 Class 是泛用的
class Box<T> {
    private contents: T;
    constructor(value: T) {
        this.contents = value;
    }
    get(): T {
        return this.contents;
    }
    set(value: T): void {
        this.contents = value;
    }
}
let stringBox = new Box<string>("hello");  // hello
let numberBox = new Box<number>(123);      // 123
開發一個函式 filterItems,該函式接受一個陣列物件格式,並回傳所有具有特定屬性的物件。使用「泛型約束」來確保每個物件都有包含該屬性
interface WithID {
    id: string;
}
// 👇 調整為泛型函式,並使用 WithID 做為泛型約束的條件
function filterItems() {
    return items.filter(item => item.id.startsWith("#"));
}
// ✅ Pass
const filteredItems = filterItems([{ id: '#a' }, { id: '123' }]);
// ❌ Error 因為陣列中的物件沒有 id 屬性
// const errorFilteredItems = filterItems([{ name: 1 }, { name: 2 }]);
每天的內容有推到 github 上喔